View.java

package example;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.Labeled;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Slider;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.Consumer;
import java.util.stream.IntStream;


/**
 * This program creates several kinds of UI components for testing.
 *
 * @author Robert C. Duvall
 */
public class View {
    // constants
    // use Java's dot notation, like with import, for properties files
    public static final String DEFAULT_RESOURCE_PACKAGE = "example.";
    public static final String DEFAULT_STYLESHEET = "/"+DEFAULT_RESOURCE_PACKAGE.replace(".", "/")+"Default.css";
    public static final boolean DEBUG = true;

    // choices displayed in UI
    private Model myModel;
    private ObservableList<String> myChoices;
    private ResourceBundle myResources;


    /**
     * Create view in given language with given model
     */
    public View (String language, Model model) {
        myModel = model;
        myChoices = FXCollections.observableArrayList("11", "22", "33", "44");
        myResources = ResourceBundle.getBundle(DEFAULT_RESOURCE_PACKAGE + "View" + language);
    }


    // create different kinds of components for testing
    // make package friendly so test classes can call it
    public Scene makeScene (int width, int height) {
        GridPane root = new GridPane();
        root.setOnMouseClicked(e -> toggleHighlight(root));
        // for example of using nested lookup
        root.getStyleClass().add("grid-pane");
        // for example of using ID lookup
        root.setId("Pane");

        // common action for components to keep it simple
        Consumer<String> updateAction = text -> myModel.setValue(text);

        // make example components
        Node label = makeLabel("Label");
        TextField inputField = makeInputField("InputField", updateAction);
        // group all buttons
        Node buttons = makeActions(
            makeButton("Apply", e -> getText(inputField, updateAction)),
            makeButton("Input", e -> getInput("InputPrompt", updateAction)),
            makeButton("Data", e -> getData("example/choices.txt", updateAction))
        );
        Node slider = makeSlider("Slider", 0, 50, 100, updateAction);
        Node picker = makeColorPicker("Picker", Color.BLUE, updateAction);
        Node combo = makeCombo("Options", myChoices, updateAction);
        Node radio = makeRadioButtons("Radio", myChoices, updateAction);
        Node check = makeCheckBoxes("Check", myChoices, updateAction);
        Node canvas = makeCanvas("Canvas", 100, 325, 20, updateAction);

        // add elements to GUI
        root.add(label, 1, 0);
        root.add(inputField, 1, 1);
        root.add(buttons, 1, 2);
        root.add(slider, 1, 3);
        root.add(picker, 1, 4);
        root.add(combo, 1, 5);
        root.add(radio, 1, 6);
        root.add(check, 1, 7);
        root.add(canvas, 2, 1, 1, 7);

        Scene scene = new Scene(root, width, height);
        // activate CSS styling
        scene.getStylesheets().add(getClass().getResource(DEFAULT_STYLESHEET).toExternalForm());
        return scene;
    }

    // Display given message as an error in the GUI
    private void showError (String message) {
        Alert alert = new Alert(AlertType.ERROR);
        alert.setTitle(myResources.getString("ErrorTitle"));
        alert.setContentText(message);
        alert.showAndWait();
    }

    // convenience methods to encapsulate making components
    private Node makeButton (String property, EventHandler<ActionEvent> response) {
        Button result = new Button();
        result.setText(myResources.getString(property));
        result.setOnAction(response);
        return setID(property, result);
    }

    private Node makeLabel (String property) {
        Label label = new Label(myResources.getString(property));
        myModel.addSetter(newText -> updateLabel(label, newText));
        return setID(property, label);
    }

    private TextField makeInputField (String id, Consumer<String> response) {
        TextField result = new TextField();
        result.setOnAction(e -> response.accept(result.getText()));
        return (TextField)setID(id, result);
    }

    private Node makeActions (Node ... buttons) {
        HBox panel = new HBox();
        panel.getStyleClass().add("button-box");
        panel.getChildren().addAll(buttons);
        return panel;
    }

    private Node makeColorPicker (String id, Color defaultChoice, Consumer<String> response) {
        ColorPicker picker = new ColorPicker(defaultChoice);
        picker.setOnAction(e -> response.accept(picker.getValue().toString()));
        return setID(id, picker);
    }

    private Node makeSlider (String id, int min, int start, int max, Consumer<String> response) {
        Slider slider = new Slider();
        slider.setMin(min);
        slider.setMax(max);
        slider.setValue(start);
        slider.valueProperty().addListener((o, oldValue, newValue) -> response.accept(newValue.toString()));
        return setID(id, slider);
    }

    // make combination of choices
    private Node makeCombo (String id, ObservableList<String> choices, Consumer<String> response) {
        // these two components seem to work the same way --- uncomment to try different flavors
        ComboBox<String> options = new ComboBox<>();
        //ChoiceBox<String> options = new ChoiceBox<>();
        options.valueProperty().addListener((o, oldValue, newValue) -> response.accept(newValue));
        // this one is only slightly different
        //ListView<String> options = new ListView<>();
        //options.getSelectionModel().selectedItemProperty().addListener((o, oldValue, newValue) -> response.accept(newValue));
        options.setItems(choices);
        return setID(id, options);
    }

    // make group of radio choices
    private Node makeRadioButtons (String idPrefix, ObservableList<String> choices, Consumer<String> response) {
        //VBox result = new VBox();
        HBox panel = new HBox();
        panel.getStyleClass().add("choice-box");
        ToggleGroup group = new ToggleGroup();
        choices.forEach(c -> {
            // these two components seems to work the same way
            //ToggleButton btn = new ToggleButton(c);
            RadioButton btn = new RadioButton(c);
            btn.setToggleGroup(group);
            panel.getChildren().add(setID(idPrefix + "-" + c, btn));
        });
        group.selectedToggleProperty().addListener((o, oldValue, newValue) -> response.accept(((Labeled)newValue.getToggleGroup().getSelectedToggle()).getText()));
        return panel;
    }

    // make group of multiple choices
    private Node makeCheckBoxes (String idPrefix, ObservableList<String> choices, Consumer<String> response) {
        VBox panel = new VBox();
        panel.getStyleClass().add("choice-box");
        choices.forEach(c -> {
            CheckBox cb = new CheckBox(c);
            cb.selectedProperty().addListener((o, oldValue, newValue) -> response.accept(String.format("%s : %b", cb.getText(), newValue)));
            panel.getChildren().add(setID(idPrefix + "-" + c, cb));
        });
        return panel;
    }

    private Node makeCanvas (String id, int width, int height, int shapeSize, Consumer<String> response) {
        Pane canvas = new Pane();
        canvas.setPrefSize(width,height);
        canvas.setOnMouseClicked(e -> {
            int x = (int)e.getX();
            int y = (int)e.getY();
            Circle c = new Circle(x, y, shapeSize / 2);
            c.getStyleClass().add("ball");
            canvas.getChildren().add(c);
            response.accept(String.format("(%d, %d)", x, y));
            // prevent any other nodes from dealing with this event
            e.consume();
        });
        canvas.getStyleClass().add("canvas");
        return setID(id, canvas);
    }

    // extract text from input field
    private static void getText(TextField inputField, Consumer<String> response) {
        response.accept(inputField.getText());
    }

    // extract text from dialog prompt
    private void getInput (String prompt, Consumer<String> response) {
        TextInputDialog dialog = new TextInputDialog(myResources.getString("InputPlaceholder"));
        dialog.setTitle(myResources.getString("InputTitle"));
        dialog.setContentText(myResources.getString(prompt));
        Optional<String> result = dialog.showAndWait();
        result.ifPresent(input -> response.accept(input));
    }

    // extract text from date file
    private void getData (String dataFile, Consumer<String> response) {
        List<String> lines = loadDataFile(dataFile);
        IntStream.range(0, lines.size()).forEach(index -> myChoices.set(index, lines.get(index).strip()));
        response.accept(dataFile);
    }

    // package friendly to allow for testing separately for errors
    List<String> loadDataFile (String dataFile) {
        try {
            return Files.readAllLines(Paths.get(View.class.getClassLoader().getResource(dataFile).toURI()));
        } catch (IOException | NullPointerException | URISyntaxException e) {
            // should not happen if project is correctly configured
            showError(String.format(myResources.getString("ResourceErrorMessage"), dataFile));
            return Collections.emptyList();
        }
    }

    // toggle CSS class used to display given node
    private void toggleHighlight (Node n) {
        final String CSS_CLASS = "highlight";
        ObservableList<String> styles = n.getStyleClass();
        if (styles.contains(CSS_CLASS)) {
            styles.remove(CSS_CLASS);
        }
        else {
            styles.add(CSS_CLASS);
        }
    }

    // factor out this one line of code from being duplicated --- too much?
    private Node setID (String id, Node node) {
        node.setId(id);
        return node;
    }

    // simple method to help debug, so all actions do the same thing
    private void updateLabel (Label lbl, String txt) {
        lbl.setText(txt);
        if (DEBUG) {
            System.out.println(lbl.getText());
        }
    }
}